@abstractframework/continuum 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +38 -17
  2. package/bin/cli.js +157 -32
  3. package/bin/hub_proxy.js +37 -17
  4. package/bin/settings.js +511 -0
  5. package/dist/assets/{abnfDiagram-VRR7QNED-DHZzhKO_.js → abnfDiagram-VRR7QNED-DkP5u0Ku.js} +2 -2
  6. package/dist/assets/{arc-9iPl5OLd.js → arc-vxPq5BjA.js} +2 -2
  7. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BVRc0NRl.js → architectureDiagram-ZJ3FMSHR-ku0NT7zM.js} +2 -2
  8. package/dist/assets/{blockDiagram-677ZJIJ3-BTIe50OR.js → blockDiagram-677ZJIJ3-DXQQcNdo.js} +2 -2
  9. package/dist/assets/{c4Diagram-LMCZKHZV-D5P-4VLr.js → c4Diagram-LMCZKHZV-DuZ33cED.js} +2 -2
  10. package/dist/assets/channel-tvH0KUyT.js +2 -0
  11. package/dist/assets/{chunk-2Q5K7J3B-WcmxYPWP.js → chunk-2Q5K7J3B-BGWsS7uu.js} +2 -2
  12. package/dist/assets/{chunk-32BRIVSS-BPArWk_S.js → chunk-32BRIVSS-BgokockY.js} +2 -2
  13. package/dist/assets/{chunk-5VM5RSS4-DplW9bbf.js → chunk-5VM5RSS4-OLY5jTMg.js} +2 -2
  14. package/dist/assets/{chunk-EX3LRPZG-B8O0P99j.js → chunk-EX3LRPZG-BWHQVZSz.js} +2 -2
  15. package/dist/assets/{chunk-JWPE2WC7-DZoiE5m4.js → chunk-JWPE2WC7-CuM7Hcrw.js} +2 -2
  16. package/dist/assets/{chunk-MOJQB5TN-FqMZZAe1.js → chunk-MOJQB5TN-DI56DwNK.js} +2 -2
  17. package/dist/assets/{chunk-RYQCIY6F-D4I_kub_.js → chunk-RYQCIY6F-phvbPmeC.js} +2 -2
  18. package/dist/assets/{chunk-V7JOEXUC-CwJZa6s3.js → chunk-V7JOEXUC-Cv7eW1t4.js} +2 -2
  19. package/dist/assets/{chunk-VR4S4FIN-CdF7hCRc.js → chunk-VR4S4FIN-D8k2SzA3.js} +2 -2
  20. package/dist/assets/{chunk-XXDRQBXY-C2vm2DXm.js → chunk-XXDRQBXY-DZy8Plz8.js} +2 -2
  21. package/dist/assets/classDiagram-OUVF2IWQ-C0x6Ou39.js +2 -0
  22. package/dist/assets/classDiagram-v2-EOCWNBFH-C0x6Ou39.js +2 -0
  23. package/dist/assets/{cose-bilkent-JH36ORCC-BEoqSVoj.js → cose-bilkent-JH36ORCC-CiwWaVUb.js} +2 -2
  24. package/dist/assets/{cynefin-VYW2F7L2-DzsDaKjB.js → cynefin-VYW2F7L2-Cz79Fwdx.js} +2 -2
  25. package/dist/assets/{cynefinDiagram-TSTJHNR4-qVt2ETEm.js → cynefinDiagram-TSTJHNR4-DYlkii-x.js} +2 -2
  26. package/dist/assets/{dagre-VKFMJZFB-BszpuHqM.js → dagre-VKFMJZFB-Cumo7o1R.js} +2 -2
  27. package/dist/assets/{diagram-FQU43EPY-DkSwN897.js → diagram-FQU43EPY-Dorm69_e.js} +2 -2
  28. package/dist/assets/{diagram-G47NLZAW-DmmkniMR.js → diagram-G47NLZAW-Bhtzc4JU.js} +2 -2
  29. package/dist/assets/{diagram-NH7WQ7WH-PkbaZ3fb.js → diagram-NH7WQ7WH-DJF-iHCK.js} +2 -2
  30. package/dist/assets/{diagram-OA4YK3LP-D5O-VGkx.js → diagram-OA4YK3LP-8mQ4915-.js} +2 -2
  31. package/dist/assets/{diagram-WEI45ONY-CtS1f8zZ.js → diagram-WEI45ONY-C4Bv6dbm.js} +2 -2
  32. package/dist/assets/{ebnfDiagram-CCIWWBDH-2ydDo0Sq.js → ebnfDiagram-CCIWWBDH-DzagnQVX.js} +2 -2
  33. package/dist/assets/{erDiagram-Q63AITRT-Dc-2Dmcc.js → erDiagram-Q63AITRT-BbkYElA6.js} +2 -2
  34. package/dist/assets/{flowDiagram-23GEKE2U-DKKOHSrp.js → flowDiagram-23GEKE2U-OcR4vkjk.js} +2 -2
  35. package/dist/assets/{ganttDiagram-NO4QXBWP-B9Zi-Zd_.js → ganttDiagram-NO4QXBWP-DyAPiIdB.js} +2 -2
  36. package/dist/assets/{gitGraphDiagram-IHSO6WYX-lXzgHn-s.js → gitGraphDiagram-IHSO6WYX-pymEefZi.js} +2 -2
  37. package/dist/assets/index-D0YdqMpf.css +1 -0
  38. package/dist/assets/index-DTn9k-3t.js +123 -0
  39. package/dist/assets/{infoDiagram-FWYZ7A6U-BHV_H3bL.js → infoDiagram-FWYZ7A6U-Dh41Som-.js} +2 -2
  40. package/dist/assets/{ishikawaDiagram-FXEZZL3T-BX74rKth.js → ishikawaDiagram-FXEZZL3T-Xc9vuvPV.js} +2 -2
  41. package/dist/assets/{journeyDiagram-5HDEW3XC-BByysGFJ.js → journeyDiagram-5HDEW3XC-bJ0gK8bC.js} +2 -2
  42. package/dist/assets/{kanban-definition-HUTT4EX6-DatfLmCp.js → kanban-definition-HUTT4EX6-Cu1Zn3KW.js} +2 -2
  43. package/dist/assets/{linear-BgTPNh4i.js → linear-DrGsraGo.js} +2 -2
  44. package/dist/assets/{mermaid.core-CrABX5SH.js → mermaid.core-CkQqPHPZ.js} +5 -5
  45. package/dist/assets/{mindmap-definition-LN4V7U3C-Bz1XgdXA.js → mindmap-definition-LN4V7U3C-ZCEP6XWH.js} +2 -2
  46. package/dist/assets/{pegDiagram-2B236MQR-D5B8Egll.js → pegDiagram-2B236MQR-DjDrn07n.js} +2 -2
  47. package/dist/assets/{pieDiagram-ENE6RG2P-BaDg4f41.js → pieDiagram-ENE6RG2P-oD3ciEy7.js} +2 -2
  48. package/dist/assets/{quadrantDiagram-ABIIQ3AL-jr1V5VXJ.js → quadrantDiagram-ABIIQ3AL-CYbDYNM7.js} +2 -2
  49. package/dist/assets/{railroadDiagram-RFXS5EU6-F13Ho8ra.js → railroadDiagram-RFXS5EU6-DMcO7O0d.js} +2 -2
  50. package/dist/assets/{requirementDiagram-TGXJPOKE-DXb_w8Ww.js → requirementDiagram-TGXJPOKE-DeeSIVn-.js} +2 -2
  51. package/dist/assets/{sankeyDiagram-HTMAVEWB-BN42kL5b.js → sankeyDiagram-HTMAVEWB-C43xzNvP.js} +2 -2
  52. package/dist/assets/{sequenceDiagram-DBY2YBRQ-DI7-dEyi.js → sequenceDiagram-DBY2YBRQ-d3lj6z8y.js} +2 -2
  53. package/dist/assets/{sizeCapture-X5ZJPWSS-Dv-15ctr.js → sizeCapture-X5ZJPWSS-D4UyeLsX.js} +2 -2
  54. package/dist/assets/{stateDiagram-2N3HPSRC-De5ObM42.js → stateDiagram-2N3HPSRC-Cq4YMMP1.js} +2 -2
  55. package/dist/assets/stateDiagram-v2-6OUMAXLB-DMVYrQgX.js +2 -0
  56. package/dist/assets/{swimlanes-5IMT3BWC-DzZwgXNi.js → swimlanes-5IMT3BWC-CRbkVLoI.js} +3 -3
  57. package/dist/assets/swimlanesDiagram-G3AALYLV-Bzg2g7oq.js +9 -0
  58. package/dist/assets/{timeline-definition-FHXFAJF6-BO8KkL_w.js → timeline-definition-FHXFAJF6-BN8oLdvs.js} +2 -2
  59. package/dist/assets/{vennDiagram-L72KCM5P-D6LUv_CX.js → vennDiagram-L72KCM5P-BqAHS4HE.js} +2 -2
  60. package/dist/assets/{wardleyDiagram-EHGQE667-DWJp0Lmi.js → wardleyDiagram-EHGQE667-D2aA14yL.js} +2 -2
  61. package/dist/assets/{xychartDiagram-FW5EYKEG-B5IA4jNf.js → xychartDiagram-FW5EYKEG-BkrnnKWS.js} +2 -2
  62. package/dist/index.html +2 -2
  63. package/package.json +2 -2
  64. package/dist/assets/channel-CEGeSBNz.js +0 -2
  65. package/dist/assets/classDiagram-OUVF2IWQ-D11Ru5dS.js +0 -2
  66. package/dist/assets/classDiagram-v2-EOCWNBFH-D11Ru5dS.js +0 -2
  67. package/dist/assets/index-CCcufdYK.css +0 -1
  68. package/dist/assets/index-DmnZnFTc.js +0 -124
  69. package/dist/assets/stateDiagram-v2-6OUMAXLB-D9AwhP3S.js +0 -2
  70. package/dist/assets/swimlanesDiagram-G3AALYLV-DyzT80_E.js +0 -9
package/README.md CHANGED
@@ -18,7 +18,8 @@ cockpit (board-first, kanban/scrum-inspired — see
18
18
  - **Backlog** — the full catalog power view (all kinds incl. recurrent /
19
19
  deprecated / trash, search, batch execute, merge, AI assist + advisor).
20
20
  - **Agents & Entities** — the workforce: the configured execution agent
21
- (executor is a pluggable seam — Codex CLI today), the advisor agent, a
21
+ (Codex CLI, Claude Code, Cursor Agent or AbstractCode — a gateway
22
+ setting), the advisor agent, a
22
23
  track record from execution history, and gateway entities with their
23
24
  skills.
24
25
  - **Team** — an [agora](docs/configuration.md#team-page-agora-hub) hub
@@ -32,7 +33,11 @@ cockpit (board-first, kanban/scrum-inspired — see
32
33
  - **Services** — control managed prod/UAT processes (start / stop /
33
34
  restart / redeploy) and write-only environment variables. This surface is
34
35
  high trust: see [SECURITY.md](SECURITY.md) before exposing it.
35
- - **Settings** — gateway session sign-in and AI preferences.
36
+ - **Settings** — gateway sign-in, execution defaults (UAT or inplace), AI
37
+ and voice preferences, and **Gateway administration**: the backlog
38
+ folder, exec runner, executor and process manager, each shown with where
39
+ its value comes from (launch flag, setting, environment (legacy), or
40
+ default). Only a gateway admin can change them.
36
41
 
37
42
  Executing a Ready item is gated by an advisory **Definition of Ready**
38
43
  checklist (type, real summary, acceptance criteria, test commands — parsed
@@ -41,16 +46,20 @@ review shows the acceptance criteria as a **Definition of Done** checklist.
41
46
 
42
47
  The observer app ([AbstractObserver](https://github.com/lpalbou/AbstractObserver))
43
48
  watches and discusses the running system; this app develops and deploys it.
44
- The pipeline is executor-agnostic by design: the gateway currently runs
45
- executions through the Codex CLI, but the executing agent is a pluggable
46
- seam and the UI reads the executor identity from the gateway's exec config
47
- (see [docs/architecture.md](docs/architecture.md)).
49
+ The pipeline is executor-agnostic: the gateway's `executor` setting picks
50
+ the agent that runs items (`codex` by default; also `claude`,
51
+ `cursor-agent`, `abstractcode`), and the UI reads the executor identity from
52
+ the gateway's exec config (see [docs/architecture.md](docs/architecture.md)).
48
53
 
49
54
  ## Install and run
50
55
 
51
- Requires Node.js ≥ 18 and a running Run Gateway
52
- ([AbstractGateway](https://github.com/lpalbou/AbstractGateway), default
53
- `http://127.0.0.1:8080`).
56
+ Requirements:
57
+
58
+ - Node.js ≥ 18.
59
+ - **AbstractGateway 0.4.1 or newer** (the supported gateway; default
60
+ `http://127.0.0.1:8080`). See
61
+ [AbstractGateway](https://github.com/lpalbou/AbstractGateway). Against an
62
+ older gateway the Board shows its *folder not available* panel.
54
63
 
55
64
  ```bash
56
65
  npx @abstractframework/continuum
@@ -63,14 +72,25 @@ Open `http://localhost:3002` and sign in with a gateway user and token (the
63
72
  connection badge at the bottom of the sidebar). The token is exchanged
64
73
  server-side for an HttpOnly session cookie and never stored in the browser.
65
74
 
66
- Common environment variables (full list in
75
+ The server is configured with launch flags or saved settings (full list in
67
76
  [docs/configuration.md](docs/configuration.md), or `abstractcontinuum --help`):
68
77
 
69
- - `PORT` (default `3002`), `HOST` (default `127.0.0.1`)
70
- - `ABSTRACTCONTINUUM_GATEWAY_URL` (or `ABSTRACTGATEWAY_URL`) — the gateway
71
- this deployment talks to (default `http://127.0.0.1:8080`)
72
- - `ABSTRACTCONTINUUM_HUB_URL` / `ABSTRACTCONTINUUM_HUB_SEAT` — the agora hub
73
- and operator seat for the Team page
78
+ ```bash
79
+ abstractcontinuum --port 3002 --gateway-url http://127.0.0.1:8080 --hub-seat alice
80
+ abstractcontinuum config set hub_seat alice # saved in ~/.abstractcontinuum/settings.json
81
+ abstractcontinuum config get # every setting and where it comes from
82
+ ```
83
+
84
+ - `--port` (default `3002`), `--host` (default `127.0.0.1`)
85
+ - `--gateway-url` — the gateway this deployment talks to (default
86
+ `http://127.0.0.1:8080`)
87
+ - `--hub-url` / `--hub-seat` / `--hub-token-file` — the agora hub, your seat
88
+ and its key for the Team page. The hub seat is also on the Settings page.
89
+
90
+ A fresh gateway needs no backlog setup: it keeps its own backlog folder, and
91
+ the Board opens on **Your backlog is empty** with **Create your first item**.
92
+ To point the Board at a project, or to enable executions, see
93
+ [docs/getting-started.md](docs/getting-started.md#your-backlog).
74
94
 
75
95
  Before exposing the console beyond localhost, read
76
96
  [docs/security.md](docs/security.md): a signed-in user can redeploy services.
@@ -87,7 +107,7 @@ git clone https://github.com/lpalbou/AbstractUIC.git abstractuic
87
107
  git clone https://github.com/lpalbou/AbstractContinuum.git abstractcontinuum
88
108
  cd abstractcontinuum
89
109
  npm install
90
- npm run dev # vite dev server on :3003 (proxies /api to :8080)
110
+ npm run dev # vite dev server on :3002 (proxies /api to the gateway)
91
111
  npm test # vitest (unit + jsdom component tests)
92
112
  npm run build # production build into dist/
93
113
  npm start # serve dist/ + the app-origin gateway session proxy on :3002
@@ -101,7 +121,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
101
121
  - [docs/getting-started.md](docs/getting-started.md) — first run and first execution
102
122
  - [docs/architecture.md](docs/architecture.md) — components, data flow, design boundaries
103
123
  - [docs/api.md](docs/api.md) — the gateway API families this app consumes
104
- - [docs/configuration.md](docs/configuration.md) — environment variables and settings
124
+ - [docs/configuration.md](docs/configuration.md) — server environment variables, gateway settings, browser settings
125
+ - [docs/conventions.md](docs/conventions.md) — the work-item grammar the Board reads
105
126
  - [docs/security.md](docs/security.md) — trust model (read before deploying)
106
127
  - [docs/faq.md](docs/faq.md) / [docs/troubleshooting.md](docs/troubleshooting.md)
107
128
  - [CHANGELOG.md](CHANGELOG.md) — release history
package/bin/cli.js CHANGED
@@ -10,52 +10,159 @@
10
10
 
11
11
  import * as http from 'http';
12
12
  import { readFileSync, existsSync, statSync } from 'fs';
13
- import { homedir } from 'os';
14
13
  import { join, extname, dirname } from 'path';
15
14
  import { fileURLToPath } from 'url';
16
15
  import { createGatewaySessionProxy } from '@abstractframework/app-server';
17
16
  import { createHubProxy } from './hub_proxy.js';
17
+ import {
18
+ SETTINGS,
19
+ SOURCE_WORDS,
20
+ apply_session_proxy_gates,
21
+ createLiveSettings,
22
+ createSettingsRoute,
23
+ default_settings_path,
24
+ display_value,
25
+ expand_home,
26
+ parse_args,
27
+ read_settings_file,
28
+ resolve_settings,
29
+ setting_spec,
30
+ update_settings_file,
31
+ } from './settings.js';
18
32
 
19
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
34
  const DIST_DIR = join(__dirname, '..', 'dist');
21
35
 
22
- const argv = process.argv.slice(2);
23
- if (argv.includes('--version') || argv.includes('-v')) {
36
+ function help_text() {
37
+ const lines = [];
38
+ const col = (left, right) => (left.length >= 30 ? ` ${left}\n${' '.repeat(32)}${right}` : ` ${left.padEnd(30)}${right}`);
39
+ lines.push('Usage:');
40
+ lines.push(' abstractcontinuum [options] start the console server');
41
+ lines.push(' abstractcontinuum config get [setting] show settings and their sources');
42
+ lines.push(' abstractcontinuum config set <setting> <value>');
43
+ lines.push(' abstractcontinuum config set hub_token --from-file <path>');
44
+ lines.push(' abstractcontinuum config unset <setting>');
45
+ lines.push(' abstractcontinuum config path print the settings file path');
46
+ lines.push('');
47
+ lines.push('Serves the AbstractContinuum console and proxies /api/* to a Run Gateway');
48
+ lines.push('through a same-origin session proxy.');
49
+ lines.push('');
50
+ lines.push('Options (each is also a saved setting; its name follows "setting"):');
51
+ for (const spec of SETTINGS) {
52
+ const left = spec.type === 'bool' ? spec.flag : `${spec.flag} ${spec.metavar}`;
53
+ const def = spec.type === 'bool' ? 'off' : spec.default === '' ? 'none' : String(spec.default);
54
+ lines.push(col(left, spec.help));
55
+ lines.push(`${' '.repeat(32)}default ${def}; setting ${spec.key}`);
56
+ if (spec.key === 'hub_token') {
57
+ lines.push(col('--hub-token-file <path>', 'read the hub token from a file (never printed)'));
58
+ lines.push(`${' '.repeat(32)}setting hub_token`);
59
+ }
60
+ }
61
+ lines.push(col('--settings-file <path>', 'settings file'));
62
+ lines.push(`${' '.repeat(32)}default ~/.abstractcontinuum/settings.json`);
63
+ lines.push(col('--help', 'show this help'));
64
+ lines.push(col('--version', 'print the version'));
65
+ lines.push('');
66
+ lines.push('On/off options take --option, --option=off or --no-option.');
67
+ lines.push('Precedence: launch flag > settings file > environment (legacy) > default.');
68
+ lines.push('The hub seat can also be set on the Settings page.');
69
+ lines.push('Legacy fallback: the environment variables listed in the configuration guide are');
70
+ lines.push('still read when neither a flag nor a setting is set.');
71
+ lines.push('');
72
+ lines.push('Documentation: https://github.com/lpalbou/AbstractContinuum#readme');
73
+ return lines.join('\n');
74
+ }
75
+
76
+ const parsed = parse_args(process.argv.slice(2));
77
+ if (parsed.error) {
78
+ console.error(`abstractcontinuum: ${parsed.error}`);
79
+ process.exit(2);
80
+ }
81
+ if (parsed.command === 'version') {
24
82
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
25
83
  console.log(pkg.version);
26
84
  process.exit(0);
27
85
  }
28
- if (argv.includes('--help') || argv.includes('-h')) {
29
- console.log(`Usage: abstractcontinuum [--help] [--version]
30
-
31
- Serves the AbstractContinuum console and proxies /api/* to a Run Gateway
32
- through a same-origin session proxy. Configuration is environment-driven:
33
-
34
- PORT HTTP port (default 3002)
35
- HOST bind address (default 127.0.0.1)
36
- ABSTRACTCONTINUUM_GATEWAY_URL gateway URL (fallback ABSTRACTGATEWAY_URL,
37
- default http://127.0.0.1:8080)
38
- ABSTRACTCONTINUUM_HUB_URL agora hub for the Team page (fallback
39
- AGORA_HUB_URL, default http://127.0.0.1:8765)
40
- ABSTRACTCONTINUUM_HUB_SEAT hub seat the Team page acts as (default laurent)
41
- ABSTRACTCONTINUUM_HUB_KEYS hub key store (default ~/.agora/keys.json)
42
- ABSTRACTCONTINUUM_HUB_KEY hub API key (overrides the key store)
43
-
44
- Documentation: https://github.com/lpalbou/AbstractContinuum#readme`);
86
+ if (parsed.command === 'help') {
87
+ console.log(help_text());
45
88
  process.exit(0);
46
89
  }
47
90
 
48
- const PORT = process.env.PORT || 3002;
91
+ const SETTINGS_PATH = parsed.settings_file || default_settings_path();
92
+
93
+ if (parsed.command === 'config') {
94
+ process.exit(run_config(parsed));
95
+ }
96
+
97
+ /** `abstractcontinuum config get|set|unset|path` — same keys as the flags. */
98
+ function run_config(p) {
99
+ const { action, key, value } = p.config;
100
+ try {
101
+ if (action === 'path') {
102
+ console.log(SETTINGS_PATH);
103
+ return 0;
104
+ }
105
+ if (action === 'set' || action === 'unset') {
106
+ let v = action === 'unset' ? null : value;
107
+ if (action === 'set' && p.from_file) {
108
+ v = readFileSync(p.from_file, 'utf8').trim();
109
+ if (!v) throw new Error(`${p.from_file} is empty`);
110
+ }
111
+ update_settings_file(SETTINGS_PATH, key, v);
112
+ const spec = setting_spec(key);
113
+ const shown = action === 'unset' ? '(removed)' : display_value(spec, read_settings_file(SETTINGS_PATH)[key]);
114
+ console.log(`${key} = ${shown} (${SETTINGS_PATH})`);
115
+ return 0;
116
+ }
117
+ // get
118
+ const { settings, problems } = resolve_settings({ flags: p.flags, file: read_settings_file(SETTINGS_PATH), env: process.env });
119
+ for (const msg of problems) console.error(`warning: ${msg}`);
120
+ if (key) {
121
+ console.log(display_value(setting_spec(key), settings[key].value));
122
+ return 0;
123
+ }
124
+ const width = Math.max(...SETTINGS.map((s) => s.key.length)) + 2;
125
+ for (const spec of SETTINGS) {
126
+ const { value: v, source } = settings[spec.key];
127
+ console.log(`${spec.key.padEnd(width)}${display_value(spec, v)} (${SOURCE_WORDS[source]})`);
128
+ }
129
+ console.log(`\nsettings file: ${SETTINGS_PATH}`);
130
+ return 0;
131
+ } catch (e) {
132
+ console.error(`abstractcontinuum config: ${e && e.message ? e.message : e}`);
133
+ return 1;
134
+ }
135
+ }
136
+
137
+ // A corrupt settings file stops the start (running on silent defaults
138
+ // would, e.g., drop a saved seat without a word).
139
+ try {
140
+ read_settings_file(SETTINGS_PATH);
141
+ } catch (e) {
142
+ console.error(`abstractcontinuum: cannot read the settings file ${SETTINGS_PATH}: ${e && e.message ? e.message : e}`);
143
+ process.exit(2);
144
+ }
145
+
146
+ const settings = createLiveSettings({
147
+ flags: parsed.flags,
148
+ settingsPath: SETTINGS_PATH,
149
+ env: process.env,
150
+ onProblem: (msg) => console.error(`warning: ${msg}`),
151
+ });
152
+ const AT_START = settings.get();
153
+
154
+ const PORT = AT_START.port.value;
49
155
  // Default bind is LOOPBACK (entity's c1768 SSRF finding against the shared
50
156
  // session proxy, plus a continuum-specific amplifier: this server also
51
157
  // mounts the hub proxy carrying the OPERATOR's seat key — a LAN peer
52
158
  // reaching the port could author hub messages as the operator). Wider
53
- // binds are an explicit deployment choice via HOST.
54
- const HOST = process.env.HOST || '127.0.0.1';
55
- const DEFAULT_GATEWAY_URL = String(process.env.ABSTRACTCONTINUUM_GATEWAY_URL || process.env.ABSTRACTGATEWAY_URL || 'http://127.0.0.1:8080').trim().replace(/\/+$/, '') || 'http://127.0.0.1:8080';
56
- const HUB_URL = String(process.env.ABSTRACTCONTINUUM_HUB_URL || process.env.AGORA_HUB_URL || 'http://127.0.0.1:8765').trim().replace(/\/+$/, '');
57
- const HUB_SEAT = String(process.env.ABSTRACTCONTINUUM_HUB_SEAT || 'laurent').trim();
58
- const HUB_KEYS_PATH = String(process.env.ABSTRACTCONTINUUM_HUB_KEYS || join(homedir(), '.agora', 'keys.json'));
159
+ // binds are an explicit deployment choice (--host).
160
+ const HOST = AT_START.host.value;
161
+ const DEFAULT_GATEWAY_URL = AT_START.gateway_url.value;
162
+
163
+ // The session proxy reads its hardening gates from the environment only;
164
+ // hand it the resolved values (flag > setting > environment > default).
165
+ apply_session_proxy_gates(AT_START, process.env);
59
166
 
60
167
  const MIME_TYPES = {
61
168
  '.html': 'text/html',
@@ -120,8 +227,8 @@ function serveFile(res, filePath) {
120
227
  }
121
228
  }
122
229
 
123
- // Cookies abstractcontinuum_gateway_*, header x-abstractcontinuum-csrf, and
124
- // the ABSTRACTCONTINUUM_* / ABSTRACTGATEWAY_* env gates all derive from appId.
230
+ // Cookies abstractcontinuum_gateway_* and header x-abstractcontinuum-csrf
231
+ // derive from appId.
125
232
  const gatewaySessionProxy = createGatewaySessionProxy({
126
233
  appId: 'abstractcontinuum',
127
234
  defaultGatewayUrl: DEFAULT_GATEWAY_URL,
@@ -130,7 +237,18 @@ const gatewaySessionProxy = createGatewaySessionProxy({
130
237
  // Team page transport: /api/hub/* forwards an allowlisted hub API subset
131
238
  // with the OPERATOR seat's key attached server-side (proposal c1692,
132
239
  // agency contract c1696 — authorship stays the operator's own).
133
- const hubProxy = createHubProxy({ hubUrl: HUB_URL, seat: HUB_SEAT, keysPath: HUB_KEYS_PATH });
240
+ // Every hub setting is a getter: a seat saved on the Settings page or with
241
+ // `abstractcontinuum config set` applies without a restart.
242
+ const hubProxy = createHubProxy({
243
+ hubUrl: () => settings.value('hub_url'),
244
+ seat: () => settings.value('hub_seat'),
245
+ keysPath: () => expand_home(settings.value('hub_keys')),
246
+ token: () => settings.value('hub_token'),
247
+ allowRemote: () => settings.value('hub_allow_remote'),
248
+ });
249
+
250
+ // The Settings page's door to this server's own settings (hub seat).
251
+ const settingsRoute = createSettingsRoute({ live: settings });
134
252
 
135
253
  const server = http.createServer((req, res) => {
136
254
  // Parse against a FIXED base: a malformed Host header (e.g. "a b") makes
@@ -149,6 +267,10 @@ const server = http.createServer((req, res) => {
149
267
  return;
150
268
  }
151
269
 
270
+ if (settingsRoute.handle(req, res, pathname)) {
271
+ return;
272
+ }
273
+
152
274
  if (hubProxy.handle(req, res, pathname, search)) {
153
275
  return;
154
276
  }
@@ -195,10 +317,13 @@ server.on('upgrade', (req, socket, head) => {
195
317
  });
196
318
 
197
319
  server.listen(PORT, HOST, () => {
320
+ const seat = AT_START.hub_seat;
198
321
  console.log(`
199
322
  AbstractContinuum is running.
200
- Local: http://localhost:${PORT}
201
- Gateway: ${DEFAULT_GATEWAY_URL}
323
+ Local: http://localhost:${PORT}
324
+ Gateway: ${DEFAULT_GATEWAY_URL}
325
+ Hub seat: ${seat.value} (${SOURCE_WORDS[seat.source]})
326
+ Settings: ${SETTINGS_PATH}
202
327
  `);
203
328
  });
204
329
 
package/bin/hub_proxy.js CHANGED
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Serves /api/hub/* by forwarding an ALLOWLISTED subset of the hub HTTP
6
6
  * API with the OPERATOR seat's key attached server-side. The key comes
7
- * from the operator's standard key store (~/.agora/keys.json) or env; it
7
+ * from the operator's standard key store (~/.agora/keys.json) or the hub_token setting; it
8
8
  * never appears in the browser, a repo, or a URL (the c1556 rule
9
9
  * generalized from charter reads to the whole chat surface).
10
10
  *
11
11
  * Contract rules honored here (agency c1696):
12
12
  * - Only allowlisted paths forward — the proxy is a door, not a tunnel.
13
13
  * - GET /api/hub/meta names the seat so the UI can render authorship
14
- * honestly ("posting as laurent").
14
+ * honestly ("posting as <seat>").
15
15
  * - Ack/read_message are ordinary POSTs the UI calls on EXPLICIT read
16
16
  * only (never on render) — enforced UI-side, but the proxy exposes
17
17
  * them as distinct paths so an audit can see which fired.
@@ -214,10 +214,10 @@ function origin_allowed(req) {
214
214
  return Boolean(host) && parsed.host.toLowerCase() === host;
215
215
  }
216
216
 
217
- function read_seat_key(keys_path, hub_url, seat) {
218
- // Env override wins (deployments without the operator key store).
219
- const env_key = String(process.env.ABSTRACTCONTINUUM_HUB_KEY || '').trim();
220
- if (env_key) return env_key;
217
+ function read_seat_key(keys_path, hub_url, seat, token) {
218
+ // An explicit token (--hub-token / --hub-token-file / the hub_token
219
+ // setting, resolved by bin/settings.js) wins over the key store.
220
+ if (token) return token;
221
221
  try {
222
222
  const store = JSON.parse(readFileSync(keys_path, 'utf8'));
223
223
  const entry = store[`${hub_url}::${seat}`];
@@ -229,14 +229,32 @@ function read_seat_key(keys_path, hub_url, seat) {
229
229
  }
230
230
  }
231
231
 
232
+ /** A value or a getter: the server hands getters so a seat saved on the
233
+ * Settings page (or with `abstractcontinuum config set`) applies without
234
+ * a restart. */
235
+ function live(v) {
236
+ return typeof v === 'function' ? v() : v;
237
+ }
238
+
239
+ /**
240
+ * opts: hubUrl, seat, keysPath, token (seat API key; overrides the key
241
+ * store), allowRemote (boolean) — each a value or a getter. Settings are
242
+ * resolved by bin/settings.js (flag > setting > environment > default);
243
+ * this module reads no environment of its own.
244
+ */
232
245
  export function createHubProxy(opts) {
233
- const hub_url = String(opts.hubUrl || '').replace(/\/+$/, '');
234
- const seat = String(opts.seat || '').trim();
235
- const keys_path = String(opts.keysPath || '');
246
+ const cfg = () => ({
247
+ hub_url: String(live(opts.hubUrl) || '').replace(/\/+$/, ''),
248
+ seat: String(live(opts.seat) || '').trim(),
249
+ keys_path: String(live(opts.keysPath) || ''),
250
+ token: String(live(opts.token) || '').trim(),
251
+ allow_remote: live(opts.allowRemote) === true,
252
+ });
236
253
 
237
254
  /** Lazily re-read per request: key rotation must not need a restart. */
238
255
  function key() {
239
- return read_seat_key(keys_path, hub_url, seat);
256
+ const c = cfg();
257
+ return read_seat_key(c.keys_path, c.hub_url, c.seat, c.token);
240
258
  }
241
259
 
242
260
  async function forward(req, res, hub_path, search) {
@@ -290,7 +308,8 @@ export function createHubProxy(opts) {
290
308
  const seat_key = key();
291
309
  if (!seat_key && !route.keyless) {
292
310
  res.writeHead(503, { 'Content-Type': 'application/json' });
293
- res.end(JSON.stringify({ error: 'hub_seat_unavailable', detail: `No hub key for seat "${seat}" (${keys_path}); set ABSTRACTCONTINUUM_HUB_KEY or provision the seat.` }));
311
+ const c = cfg();
312
+ res.end(JSON.stringify({ error: 'hub_seat_unavailable', detail: `No hub key for seat "${c.seat}" in ${c.keys_path}. Provision the seat there, or start Continuum with --hub-token-file <path>.` }));
294
313
  return;
295
314
  }
296
315
 
@@ -344,7 +363,7 @@ export function createHubProxy(opts) {
344
363
  // metadata). Every other write: application/json.
345
364
  upstream_headers['Content-Type'] = route.upload ? String(req.headers['content-type'] || 'application/octet-stream') : 'application/json';
346
365
  }
347
- const r = await fetch(`${hub_url}${hub_path}${search || ''}`, {
366
+ const r = await fetch(`${cfg().hub_url}${hub_path}${search || ''}`, {
348
367
  method: req.method,
349
368
  headers: upstream_headers,
350
369
  body: body && req.method !== 'GET' ? body : undefined,
@@ -415,7 +434,7 @@ export function createHubProxy(opts) {
415
434
  // as the operator seat (adversary P0).
416
435
  const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
417
436
  const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
418
- const allow_remote = String(process.env.ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE || '').trim() === '1';
437
+ const allow_remote = cfg().allow_remote;
419
438
  const seat_key = key();
420
439
  let WebSocketServer, WebSocket;
421
440
  try {
@@ -433,7 +452,7 @@ export function createHubProxy(opts) {
433
452
  // never negotiated (live find: browsers/ws clients offer it by
434
453
  // default and the pipe corrupts).
435
454
  if (!this._wss) this._wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
436
- const hub_ws_url = hub_url.replace(/^http/, 'ws') + '/ws';
455
+ const hub_ws_url = cfg().hub_url.replace(/^http/, 'ws') + '/ws';
437
456
  const upstream = new WebSocket(hub_ws_url, {
438
457
  headers: { Authorization: `Bearer ${seat_key}` },
439
458
  perMessageDeflate: false,
@@ -521,10 +540,10 @@ export function createHubProxy(opts) {
521
540
  // this is defense-in-depth if HOST is widened deliberately.
522
541
  const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
523
542
  const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
524
- const allow_remote = String(process.env.ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE || '').trim() === '1';
543
+ const allow_remote = cfg().allow_remote;
525
544
  if (!loopback && !allow_remote) {
526
545
  res.writeHead(403, { 'Content-Type': 'application/json' });
527
- res.end(JSON.stringify({ error: 'hub_proxy_non_loopback', detail: `Hub proxy refuses non-loopback peer ${peer} (it authors as the operator seat). Set ABSTRACTCONTINUUM_HUB_ALLOW_REMOTE=1 only behind a trusted front.` }));
546
+ res.end(JSON.stringify({ error: 'hub_proxy_non_loopback', detail: `Hub proxy refuses non-loopback peer ${peer} (it authors as the operator seat). Start Continuum with --hub-allow-remote only behind a trusted front.` }));
528
547
  return true;
529
548
  }
530
549
  // Cross-origin browser requests are refused even from loopback: the
@@ -538,7 +557,8 @@ export function createHubProxy(opts) {
538
557
 
539
558
  if (pathname === '/api/hub/meta') {
540
559
  res.writeHead(200, { 'Content-Type': 'application/json' });
541
- res.end(JSON.stringify({ ok: true, hub_url, seat, seat_key_present: Boolean(key()) }));
560
+ const c = cfg();
561
+ res.end(JSON.stringify({ ok: true, hub_url: c.hub_url, seat: c.seat, seat_key_present: Boolean(key()) }));
542
562
  return true;
543
563
  }
544
564
  const hub_path = pathname.slice('/api/hub'.length);